using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Web.Services;
using System.Web.Services.Protocols;
using Microsoft.CSharp;

namespace WebServiceClientGenerator
{
    /// <summary>
    /// Abstract class that implements the <see cref="ICodeWriter"/> and
    /// provides the common methods for web service client code generating.
    /// </summary>
    public abstract class AbstractClientGenerator : AbstractGenerator
    {
        #region Private Static Fields

        private static readonly CodeTypeReference _typeSoapHttpClientProtocol =
            CodeUtils.NewGlobalTypeReference(typeof(SoapHttpClientProtocol));

        private static readonly CodeTypeReference _typeWebServiceBindingAttribute =
            CodeUtils.NewGlobalTypeReference(typeof(WebServiceBindingAttribute));

        private static readonly CodeTypeReference _objectArrayType =
            CodeUtils.NewGlobalTypeReference(typeof(object[]));

        private static readonly CodeAttributeDeclaration _soapDocumentMethodAttribute =
            new CodeAttributeDeclaration(
                CodeUtils.NewGlobalTypeReference(typeof(SoapDocumentMethodAttribute)));

        #endregion

        #region Public Methods

        /// <summary>
        /// Declares the web service client class type and implements 
        /// <paramref name="interfaces"/>.
        /// </summary>
        /// <param name="className">The name of the declared class.</param>
        /// <param name="xmlNamespace">
        /// XML namespace of the Web Service.
        /// </param>
        /// <param name="interfaces">
        /// Interfaces that the class will implement.
        /// </param>
        /// <returns>
        /// An instance of <see cref="CodeTypeDeclaration"/> that can used
        /// to generate source code.
        /// </returns>
        public static CodeTypeDeclaration GenerateClassCode(
            string className, string xmlNamespace, ICollection<Type> interfaces)
        {
            CodeUtils.CheckStringArgumentEmptyOrNull(className, "className");
            CodeUtils.CheckStringArgumentEmptyOrNull(xmlNamespace, "xmlNamespace");
            CodeUtils.CheckCollectionArgumentEmptyOrNull(interfaces, "interfaces");

            //Create web service binding attribute
            CodeAttributeDeclaration webServiceBindingAttribute =
                new CodeAttributeDeclaration(
                    _typeWebServiceBindingAttribute,
                    new CodeAttributeArgument(
                        "Namespace",
                        new CodePrimitiveExpression(xmlNamespace)
                        )
                    );

            //Create Class Declaration
            CodeTypeReference[] codeInterfaces =
                new CodeTypeReference[interfaces.Count];
            int index = 0;
            foreach (Type type in interfaces)
            {
                if (!type.IsInterface)
                {
                    throw new ArgumentException(
                        "Not an interface type: " + type);
                }
                codeInterfaces[index++] = CodeUtils.NewGlobalTypeReference(type);
            }

            CodeTypeDeclaration codeClass = CodeUtils.DefineClass(
                new CodeAttributeDeclaration[] { webServiceBindingAttribute },
                TypeAttributes.Public,
                className,
                _typeSoapHttpClientProtocol,
                codeInterfaces);

            // implements interface
            codeClass.Members.AddRange(ImplementInterface(interfaces));

            // Add dummy ServiceInterface property
            CodeMemberProperty codeServiceInterfaceProperty =
                new CodeMemberProperty();
            codeServiceInterfaceProperty.HasSet = true;
            codeServiceInterfaceProperty.Attributes = MemberAttributes.Public;
            codeServiceInterfaceProperty.Name = "ServiceInterface";
            codeServiceInterfaceProperty.Type = CodeUtils.NewGlobalTypeReference(typeof(Type));

            codeClass.Members.Add(codeServiceInterfaceProperty);

            // Add dummy Tag property
            CodeMemberProperty codeTagProperty =
                new CodeMemberProperty();
            codeTagProperty.HasSet = true;
            codeTagProperty.Attributes = MemberAttributes.Public;
            codeTagProperty.Name = "Tag";
            codeTagProperty.Type = CodeUtils.NewGlobalTypeReference(typeof(object));

            codeClass.Members.Add(codeTagProperty);

            return codeClass;
        }

        #endregion

        #region Protected Instance Methods

        /// <summary>
        /// Generate code to disable XML Documentation warning for the
        /// later generated source code.
        /// </summary>
        /// <param name="writer">To where the code is written.</param>
        /// <param name="generator">An <see cref="ICodeGenerator"/>.</param>
        /// <param name="options">Code generator options.</param>
        protected void RestoreDocumentationWarning(
            TextWriter writer, 
            ICodeGenerator generator, 
            CodeGeneratorOptions options)
        {
            // TODO: not sure what to do for VB and other langurages
            if (this.CodeDomProvider is CSharpCodeProvider)
            {
                generator.GenerateCodeFromCompileUnit(
                    new CodeSnippetCompileUnit("#pragma warning restore 1591"),
                    writer, options ?? this.CodeGeneratorOptions);
            }
        }

        /// <summary>
        /// Generate code to disable XML Documentation warning for the
        /// generated source code.
        /// </summary>
        /// <param name="writer">To where the code is written.</param>
        /// <param name="generator">An <see cref="ICodeGenerator"/>.</param>
        /// <param name="options">Code generator options.</param>
        protected void DisableDocumentationWarning(
            TextWriter writer,
            ICodeGenerator generator,
            CodeGeneratorOptions options)
        {
            // TODO: not sure what to do for VB and other langurages
            if (this.CodeDomProvider is CSharpCodeProvider)
            {
                generator.GenerateCodeFromCompileUnit(
                    new CodeSnippetCompileUnit("#pragma warning disable 1591"),
                    writer, options ?? this.CodeGeneratorOptions);
            }
        }

        #endregion

        #region Private Static Methods

        // implements all the methods defined in the interface.
        private static CodeTypeMemberCollection ImplementInterface(IEnumerable<Type> interfaces)
        {
            Dictionary<MethodSignature, CodeTypeReferenceCollection> methodMap =
                new Dictionary<MethodSignature, CodeTypeReferenceCollection>();

            foreach (Type type in interfaces)
            {
                AddMethodsToMap(methodMap, type);
                foreach (Type parent in type.GetInterfaces())
                {
                    AddMethodsToMap(methodMap, parent);
                }
            }

            CodeTypeMemberCollection codeMethods = new CodeTypeMemberCollection();
            foreach (KeyValuePair<MethodSignature, CodeTypeReferenceCollection> pair in methodMap)
            {
                codeMethods.Add(ImplementWebMethod(pair.Key, pair.Value));
            }

            return codeMethods;
        }

        private static void AddMethodsToMap(
            IDictionary<MethodSignature, CodeTypeReferenceCollection> methodMap, Type type)
        {
            foreach (MethodInfo info in type.GetMethods())
            {
                MethodSignature signature = new MethodSignature(info);
                CodeTypeReferenceCollection interfaceCollection;
                if (!methodMap.TryGetValue(signature, out interfaceCollection))
                {
                    interfaceCollection = new CodeTypeReferenceCollection();
                    methodMap[signature] = interfaceCollection;
                }
                interfaceCollection.Add(
                    CodeUtils.NewGlobalTypeReference(info.DeclaringType));
            }
        }

        // implement one web method
        private static CodeMemberMethod ImplementWebMethod(MethodSignature method,
            CodeTypeReferenceCollection interfaceCollection)
        {
            CodeTypeReference returnType =
                CodeUtils.NewGlobalTypeReference(method.ReturnType);

            CodeMemberMethod codeMethod = CodeUtils.DefineMethod(
                new CodeAttributeDeclaration[] { _soapDocumentMethodAttribute },
                MemberAttributes.Public,
                returnType,
                method.Name);

            codeMethod.ImplementationTypes.AddRange(interfaceCollection);

            // determine if return type is void
            bool isVoid = (method.ReturnType == typeof(void));

            // Define all parameters and seperate them by in/out parameter
            // type. Ref type is considered both in and out.
            List<CodeVariableReferenceExpression> inParams =
                new List<CodeVariableReferenceExpression>();

            List<OutParam> outParams = new List<OutParam>();

            foreach (MethodSignature.Parameter param in method.Parameters)
            {
                CodeTypeReference codeParamType =
                    CodeUtils.NewGlobalTypeReference(param.Type);

                FieldDirection direction =param.FieldDirection;

                CodeVariableReferenceExpression codeParam =
                    CodeUtils.DefineParameter(
                        codeMethod, codeParamType, direction, param.Name);

                if (direction != FieldDirection.Out) // In or Ref
                {
                    inParams.Add(codeParam);
                }

                if (direction != FieldDirection.In) // Out or Ref
                {
                    //outParamTypes.Add(codeParamType);
                    outParams.Add(new OutParam(codeParamType, codeParam));
                }

            }

            // Calls the Invoke method on base class with all 'in' parameters
            CodeMethodInvokeExpression invokation = new CodeMethodInvokeExpression(
                new CodeThisReferenceExpression(), "Invoke",
                new CodePrimitiveExpression(method.Name),
                new CodeArrayCreateExpression(_objectArrayType, inParams.ToArray())
                );

            // Define the result variable and initialize it with the result of
            // the invoke method only if return type is not void.
            CodeVariableReferenceExpression result = null;

            if (isVoid && outParams.Count == 0)
            {
                codeMethod.Statements.Add(invokation);
            }
            else
            {
                result = CodeUtils.DefineVariable(
                    codeMethod, _objectArrayType, "result", invokation);
            }


            // Out parameter start from index
            int index = isVoid ? 0 : 1;

            // Assign result to 'out' parameters
            foreach (OutParam outParam in outParams)
            {
                codeMethod.Statements.Add(
                    new CodeAssignStatement(
                        outParam.Variable,
                        new CodeCastExpression(outParam.Type,
                            CodeUtils.ArrayIndex(result, index++))
                        )
                    );
            }

            if (!isVoid)
            {
                // Define the return statement
                CodeMethodReturnStatement codeReturn =
                    new CodeMethodReturnStatement();

                // Return the result if return type is not void.
                codeReturn.Expression = new CodeCastExpression(
                    returnType, CodeUtils.ArrayIndex(result, 0));
                codeMethod.Statements.Add(codeReturn);
            }

            return codeMethod;
        }

        #endregion

        private struct OutParam
        {
            public CodeTypeReference Type;
            public CodeVariableReferenceExpression Variable;

            public OutParam(
                CodeTypeReference type,
                CodeVariableReferenceExpression variable)
            {
                Type = type;
                Variable = variable;
            }
        }
    }
}